using System;
using System.Collections;
using System.Collections.Generic;
using System.Data;
using System.Diagnostics;
using System.Globalization;
using System.IO;
using System.Runtime.InteropServices;
using System.Xml;

using FinlogiK.FxCop.Statistics.XsltTransformation;


namespace FinlogiK.FxCop.Statistics
{
	/// <summary>
	/// Collects violation statistics for an FxCop project.
	/// </summary>
	public sealed class StatisticsCollector
	{
		#region Fields

		private readonly string _connectionString;
		private readonly string _projectName;
		private readonly ProjectConfiguration _projectConfiguration;

		private event EventHandler<AnalysisOperationStartingEventArgs> _analysisOperationStarting;
		private event EventHandler<AnalysisOperationEndedEventArgs> _analysisOperationEnded;

		#endregion

		#region Constructors

		/// <summary>
		/// Initializes a new instance of the <see cref="StatisticsCollector"/> class.
		/// </summary>
		/// <param name="connectionString">The database connection string.</param>
		/// <param name="projectName">The name of the FxCop project for which statistics should be collected.</param>
		public StatisticsCollector(string connectionString, string projectName)
			: this(connectionString, projectName, null, null)
		{
		}

		/// <summary>
		/// Initializes a new instance of the <see cref="StatisticsCollector"/> class.
		/// </summary>
		/// <param name="connectionString">The database connection string.</param>
		/// <param name="projectName">The name of the FxCop project for which statistics should be collected.</param>
		/// <param name="projectPath">The full path of the FxCop project file.</param>
		/// <param name="fxCopCmdPath">The full path to the copy of FxCopCmd.exe that should be used for the analyses.</param>
		public StatisticsCollector(string connectionString, string projectName, string projectPath, string fxCopCmdPath)
		{
			if (connectionString == null) throw new ArgumentNullException("connectionString");

			this._connectionString = connectionString;
			this._projectName = projectName;
			this._projectConfiguration = this.ReadProjectConfiguration(projectPath, fxCopCmdPath);
		}

		#endregion

		#region Events

		/// <summary>
		/// Occurs when a new operation is starting as part of a statistics collection run.
		/// </summary>
		public event EventHandler<AnalysisOperationStartingEventArgs> AnalysisOperationStarting
		{
			add
			{
				this._analysisOperationStarting += value;
			}
			remove
			{
				this._analysisOperationStarting -= value;
			}
		}

		/// <summary>
		/// Occurs when an operation has completed as part of a statistics collection run.
		/// </summary>
		public event EventHandler<AnalysisOperationEndedEventArgs> AnalysisOperationEnded
		{
			add
			{
				this._analysisOperationEnded += value;
			}
			remove
			{
				this._analysisOperationEnded -= value;
			}
		}

		#endregion

		#region Methods

		/// <summary>
		/// Collects violation summary statistics for the FxCop project.
		/// </summary>
		/// <remarks>
		///		The current UTC date/time is used for the analysis timestamp.
		/// 	<list type="number">
		/// 		<listheader>The following steps are followed as part of statistics collection:</listheader>
		/// 		<item><description>A working copy of the FxCop project file is created, and all subsequent steps operate against this copy.</description></item>
		/// 		<item><description>TODO exclusions are removed.</description></item>
		/// 		<item><description>FxCop is run, and the resulting violation statistics are saved to the database as TODO violations.</description></item>
		/// 		<item><description>All rules in the project are enabled.</description></item>
		/// 		<item><description>FxCop is run again, and the resulting violation statistics are saved to the database as total violations.</description></item>
		/// 		<item><description>The working copy of the project file is deleted.</description></item>
		/// 	</list>
		/// </remarks>
		public DateTime CollectStatistics()
		{
			return this.CollectStatistics(DateTime.UtcNow);
		}

		/// <summary>
		/// Collects violation summary statistics for the FxCop project.
		/// </summary>
		/// <param name="analysisTimestamp">The UTC timestamp to be stored for the analysis run.</param>
		/// <remarks>
		/// 	<list type="number">
		/// 		<listheader>The following steps are followed as part of statistics collection:</listheader>
		/// 		<item><description>A working copy of the FxCop project file is created, and all subsequent steps operate against this copy.</description></item>
		/// 		<item><description>TODO exclusions are removed.</description></item>
		/// 		<item><description>FxCop is run, and the resulting violation statistics are saved to the database as TODO violations.</description></item>
		/// 		<item><description>All rules in the project are enabled.</description></item>
		/// 		<item><description>FxCop is run again, and the resulting violation statistics are saved to the database as total violations.</description></item>
		/// 		<item><description>The working copy of the project file is deleted.</description></item>
		/// 	</list>
		/// </remarks>
		public DateTime CollectStatistics(DateTime analysisTimestamp)
		{
			bool finished = false;

			string workingProjectPath = Path.Combine(Path.GetDirectoryName(this._projectConfiguration.ProjectFilePath), Path.GetFileName(Path.GetTempFileName()));
			File.Copy(this._projectConfiguration.ProjectFilePath, workingProjectPath);

			try
			{
				if (this.RunOperation(AnalysisOperation.TodoExclusionRemoval, workingProjectPath, analysisTimestamp))
				{
					if (this.RunOperation(AnalysisOperation.TodoViolationDetection, workingProjectPath, analysisTimestamp))
					{
						if (this.RunOperation(AnalysisOperation.TodoViolationStatisticsPersistence, workingProjectPath, analysisTimestamp))
						{
							if (this.RunOperation(AnalysisOperation.RuleActivation, workingProjectPath, analysisTimestamp))
							{
								if (this.RunOperation(AnalysisOperation.TotalViolationDetection, workingProjectPath, analysisTimestamp))
								{
									if (this.RunOperation(AnalysisOperation.TotalViolationStatisticsPersistence, workingProjectPath, analysisTimestamp))
									{
										finished = true;
									}
								}
							}
						}
					}
				}
			}
			finally
			{
				File.Delete(workingProjectPath);

				if (!finished)
				{
					StatisticsPersister persister = new StatisticsPersister(this._connectionString);
					persister.RemoveAllStatistics(this._projectConfiguration.ProjectId, analysisTimestamp);
				}
			}

			return analysisTimestamp;
		}

		private bool RunOperation(AnalysisOperation operation, string workingProjectPath, DateTime analysisTimestamp)
		{
			DateTime startTime = DateTime.Now;
			bool cancelled = !this.OnAnalysisOperationStarting(operation);

			if (!cancelled)
			{
				switch (operation)
				{
					case AnalysisOperation.TodoExclusionRemoval:
						this.RemoveTodoExclusions(workingProjectPath);
						break;
					case AnalysisOperation.TodoViolationDetection:
						this.RunFxCopAnalysis(workingProjectPath);
						break;
					case AnalysisOperation.TodoViolationStatisticsPersistence:
						this.SaveStatistics(workingProjectPath, analysisTimestamp, ViolationStatisticType.Todo);
						break;
					case AnalysisOperation.RuleActivation:
						StatisticsCollector.ActivateAllRules(workingProjectPath);
						break;
					case AnalysisOperation.TotalViolationDetection:
						this.RunFxCopAnalysis(workingProjectPath);
						break;
					case AnalysisOperation.TotalViolationStatisticsPersistence:
						this.SaveStatistics(workingProjectPath, analysisTimestamp, ViolationStatisticType.Total);
						break;
				}

				this.OnAnalysisOperationEnded(operation, startTime);
			}

			return !cancelled;
		}

		private void RemoveTodoExclusions(string workingProjectPath)
		{
			XmlDocument document = StatisticsCollector.OpenFxCopProject(workingProjectPath);

			if (this._projectConfiguration.TreatAllExclusionsAsTodos)
			{
				foreach (XmlElement messageElement in document.DocumentElement.SelectNodes("FxCopReport/Targets/Target/descendant::Message[@Status = \"Excluded\"]"))
				{
					messageElement.SetAttribute("Status", "Active");
				}
			}
			else
			{
				IList<Note> noteDefinitions = this.GetNotes(document);
				if (noteDefinitions.Count > 0)
				{
					foreach (XmlElement messageElement in document.DocumentElement.SelectNodes("FxCopReport/Targets/Target/descendant::Message[@Status = \"Excluded\"]"))
					{
						Note firstNote = StatisticsCollector.ExtractFirstNote(noteDefinitions, messageElement.SelectNodes("Notes/User"));
						if ((firstNote != null) && (firstNote.IsTodoNote)) messageElement.SetAttribute("Status", "Active");
					}

					document.Save(workingProjectPath);
				}
			}
		}

		private static Note ExtractFirstNote(IList<Note> sortedNotes, XmlNodeList nodes)
		{
			Note result = null;

			if (nodes.Count > 0)
			{
				IList<Note> notes = new List<Note>();
				foreach (XmlElement userElement in nodes)
				{
					string userName = userElement.GetAttribute("Name");
					foreach (XmlElement noteElement in userElement.GetElementsByTagName("Note"))
					{
						Note note = new Note(userName,
							int.Parse(noteElement.GetAttribute("Id"), CultureInfo.InvariantCulture),
							false);
						notes.Add(note);
					}
				}

				if (notes.Count > 0)
				{
					foreach (Note noteDefinition in sortedNotes)
					{
						if (notes.Contains(noteDefinition))
						{
							result = noteDefinition.Clone();
							break;
						}
					}
				}
			}

			return result;
		}

		private IList<Note> GetNotes(XmlDocument projectDocument)
		{
			XmlNodeList notesUserElements = projectDocument.DocumentElement.SelectNodes("FxCopReport/Notes/User");
			SortedList<DateTime, Note> notes = new SortedList<DateTime, Note>();

			IList<string> prefixes = this.ReadTodoPrefixes();

			foreach (XmlElement notesUserElement in notesUserElements)
			{
				XmlNodeList noteElements = notesUserElement.SelectNodes("Note");
				string userName = notesUserElement.GetAttribute("Name");

				foreach (XmlElement noteElement in noteElements)
				{
					string note = noteElement.InnerText.Trim();
					bool isTodoNote = false;
					foreach (string prefix in prefixes)
					{
						if (note.StartsWith(prefix, StringComparison.OrdinalIgnoreCase))
						{
							isTodoNote = true;
							break;
						}
					}

					DateTime modificationTimestamp = DateTime.ParseExact(noteElement.GetAttribute("Modified"), "u", CultureInfo.InvariantCulture);

					notes.Add(modificationTimestamp, new Note(userName,
						int.Parse(noteElement.GetAttribute("Id"), CultureInfo.InvariantCulture), isTodoNote));
				}
			}

			List<Note> result = new List<Note>(notes.Values);
			result.Reverse();	// Sort in descending order by last modification timestamp.
			return result;
		}

		private int SaveStatistics(string workingProjectPath, DateTime analysisTimestamp, ViolationStatisticType violationStatisticType)
		{
			XmlDocument summaryData = TransformerFactory.CreateTransformer(TransformationType.Summary).TransformFxCopOutput(workingProjectPath);

			StatisticsPersister persister = new StatisticsPersister(this._connectionString);
			return persister.SaveSummaryStatistics(this._projectConfiguration.ProjectId, analysisTimestamp, violationStatisticType,
				summaryData);
		}

		private static void ActivateAllRules(string workingProjectPath)
		{
			XmlDocument document = StatisticsCollector.OpenFxCopProject(workingProjectPath);
			foreach (XmlElement ruleFileElement in document.DocumentElement.SelectNodes("Rules/RuleFiles/RuleFile"))
			{
				ruleFileElement.SetAttribute("Enabled", "True");
				ruleFileElement.SetAttribute("AllRulesEnabled", "True");

				while (ruleFileElement.HasChildNodes)
				{
					ruleFileElement.RemoveChild(ruleFileElement.FirstChild);
				}
			}
			document.Save(workingProjectPath);
		}

		private static XmlDocument OpenFxCopProject(string path)
		{
			XmlDocument document = new XmlDocument();
			document.Load(path);

			return document;
		}

		[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Usage", "CA2201:DoNotRaiseReservedExceptionTypes", Justification="Used to correctly return error when integrated with build tools.")]
		private void RunFxCopAnalysis(string workingProjectPath)
		{
			ProcessStartInfo startInfo = new ProcessStartInfo(this._projectConfiguration.FxCopCmdPath,
				string.Format(CultureInfo.InvariantCulture, @"/project:""{0}"" /update", workingProjectPath));
			startInfo.CreateNoWindow = true;
			startInfo.UseShellExecute = false;

			using (Process process = Process.Start(startInfo))
			{
				process.WaitForExit();
				switch (process.ExitCode)
				{
					case 0:		// No error.
					case 0x400:	// Only problems are build-breaking violations.
						// Continue with analysis.
						break;
					case 0x2:	// Only problems are Rule exceptions... on continue pareil!
						break;
					default:
						throw new ExternalException(string.Format(CultureInfo.InvariantCulture,
							"FxCopCmd.exe exited with code {0}.", process.ExitCode));
				}
			}
		}

		private ProjectConfiguration ReadProjectConfiguration(string projectPath, string fxCopCmdPath)
		{
			using (IDbConnection connection = DbUtility.OpenConnection(this._connectionString))
			using (IDbCommand command = connection.CreateCommand())
			{
				command.CommandText = "spGetProjectByName";
				command.CommandType = CommandType.StoredProcedure;

				DbUtility.AddParameter(command, "ProjectName", DbType.String, 100, this._projectName);

				using (IDataReader reader = command.ExecuteReader(CommandBehavior.SingleRow))
				{
					reader.Read();
					return new ProjectConfiguration(reader.GetInt32(0), projectPath ?? reader.GetString(2),
						fxCopCmdPath ?? reader.GetString(3), reader.GetBoolean(1));
				}
			}
		}

		private bool OnAnalysisOperationStarting(AnalysisOperation operation)
		{
			bool cancelled = false;

			if (this._analysisOperationStarting != null)
			{
				AnalysisOperationStartingEventArgs eventArgs = new AnalysisOperationStartingEventArgs(operation);
				this._analysisOperationStarting(this, eventArgs);
				cancelled = eventArgs.Cancel;
			}

			return !cancelled;
		}

		private void OnAnalysisOperationEnded(AnalysisOperation operation, DateTime startTime)
		{
			if (this._analysisOperationEnded != null)
			{
				this._analysisOperationEnded(this, new AnalysisOperationEndedEventArgs(operation, DateTime.Now - startTime));
			}
		}

		private IList<string> ReadTodoPrefixes()
		{
			using (IDbConnection connection = DbUtility.OpenConnection(this._connectionString))
			using (IDbCommand command = connection.CreateCommand())
			{
				command.CommandText = "spGetTodoPrefixes";
				command.CommandType = CommandType.StoredProcedure;

				using (IDataReader reader = command.ExecuteReader())
				{
					IList<string> prefixes = new List<string>();

					while (reader.Read())
					{
						prefixes.Add(reader.GetString(0));
					}

					return prefixes;
				}
			}
		}

		#endregion

		#region Nested classes

		private sealed class ProjectConfiguration
		{
			#region Fields

			private readonly int _projectId;
			private readonly string _projectFilePath;
			private readonly string _fxCopCmdPath;
			private readonly bool _treatAllExclusionsAsTodos;

			#endregion

			#region Constructors

			internal ProjectConfiguration(int projectId, string projectFilePath, string fxCopCmdPath, bool treatAllExclusionsAsTodos)
			{
				this._projectId = projectId;
				this._projectFilePath = projectFilePath;
				this._fxCopCmdPath = fxCopCmdPath;
				this._treatAllExclusionsAsTodos = treatAllExclusionsAsTodos;
			}

			#endregion

			#region Properties

			internal int ProjectId
			{
				get
				{
					return this._projectId;
				}
			}

			internal string ProjectFilePath
			{
				get
				{
					return this._projectFilePath;
				}
			}

			internal string FxCopCmdPath
			{
				get
				{
					return this._fxCopCmdPath;
				}
			}

			internal bool TreatAllExclusionsAsTodos
			{
				get
				{
					return this._treatAllExclusionsAsTodos;
				}
			}

			#endregion
		}

		private sealed class Note
		{
			#region Fields

			private string _userName;
			private int _id;
			private bool _isTodoNote;

			#endregion

			#region Constructors

			internal Note(string userName, int id, bool isTodoNote)
			{
				this._userName = userName;
				this._id = id;
				this._isTodoNote = isTodoNote;
			}

			#endregion

			#region Properties

			internal string UserName
			{
				get
				{
					return this._userName;
				}
			}

			internal int Id
			{
				get
				{
					return this._id;
				}
			}

			internal bool IsTodoNote
			{
				get
				{
					return this._isTodoNote;
				}
			}

			#endregion

			#region Methods

			internal Note Clone()
			{
				return new Note(this.UserName, this.Id, this.IsTodoNote);
			}

			public override bool Equals(object obj)
			{
				bool result = false;
				if (obj != null)
				{
					Note note = obj as Note;
					if (note != null) result = this.Equals(note);
				}
				return result;
			}

			internal bool Equals(Note note)
			{
				bool result = false;
				if (note != null)
				{
					result = (this.UserName == note.UserName) && (this.Id == note.Id);
				}
				return result;
			}

			public override int GetHashCode()
			{
				return this.UserName.GetHashCode() ^ this.Id.GetHashCode();
			}


			#endregion
		}

		#endregion
	}
}
